/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * This class provides utility methods for primitive loading. <br/>
 * Primitives are discovered either using annotations or static fields
 * (<tt>EXPORTED_NAMES</tt> and <tt>EXPORTED_ARITIES</tt>).
 * However the 'fields' way of discovering primitives should be considered
 * deprecated.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
final class Primitives {

    /** Name of field containing exported primitive names. */
    private static final String EXPORTED_NAMES_FIELD = "EXPORTED_NAMES";

    /** Name of field containing arity of exported primitives. */
    private static final String EXPORTED_ARITIES_FIELD = "EXPORTED_ARITIES";

    /** Message for duplicate primitive definition. */
    private static final String PRIM_DEF_TWICE =
        "'%s' primive defined twice";

    /** Message for invalid primitive provider. */
    private static final String INVALID_PROVIDER =
        "invalid primitive provider '%s' (should contain both 'EXPORTED_NAMES' and 'EXPORTED_ARITIES' fields)";

    /** Message for invalid primitive arity. */
    private static final String INVALID_ARITY_FOR_PRIM =
        "%s: invalid arity: %d";

    /** Message for unaccessible export field. */
    private static final String UNACCESSIBLE_EXPORT_FIELD =
        "%s: both 'EXPORTED_NAMES' and 'EXPORTED_ARITIES' fields should be accessible";

    /** Message for incoherent fields. **/
    private static final String INCOHERENT_EXPORT_FIELDS =
        "%s: both 'EXPORTED_NAMES' and 'EXPORTED_ARITIES' fields should have same size";

    /** Message for missing implementation of exported primitive. */
    private static final String NO_IMPLEMENTATION_OF_EXPORTED =
        "%s: no implementation of exported '%s' primitive";

    /** Message for missing implementation of primitive. */
    private static final String NO_IMPLEMENTATION =
        "no implementation for '%s' primitive";

    /** Annotation for primitive providers. */
    private static final Class<PrimitiveProvider> PRIMITIVE_PROVIDER_ANNOT =
        PrimitiveProvider.class;

    /** Annotation for primitives. */
    private static final Class<Primitive> PRIMITIVE_ANNOT =
        Primitive.class;

    /**
     * No instance of this class.
     */
    private Primitives() {
    } // end empty constructor

    /**
     * Looks for a given primitive in a list of classes. <br/>
     * The primitive implementation is taken from the first class implementing
     * the primitive.
     * @param primName name of primitive to look for
     *                 - should not be <tt>null</tt>
     * @param classes list of classes to search for primitive implementation
     *                - should not be <tt>null</tt>
     * @return a method implementing the primitive
     * @throws CadmiumException if one of the classes is not a valid
     *                              primitive provider
     * @throws CadmiumException if no implementation is found
     */
    static Method lookupPrimitive(final String primName,
                                  final Class<?>... classes)
        throws CadmiumException {
        assert primName != null : "null primName";
        assert classes != null : "null classes";
        for (Class<?> cl : classes) {
            final Method res =
                (cl.getAnnotation(Primitives.PRIMITIVE_PROVIDER_ANNOT) != null)
                ? lookupPrimitiveByAnnot(primName, cl)
                : lookupPrimitiveByFields(primName, cl);
            if (res != null) {
                return res;
            } // end if
        } // end for
        throw new CadmiumException(String.format(Primitives.NO_IMPLEMENTATION, primName));
    } // end method 'lookupPrimitive(String, Class...)'

    /**
     * Looks for a given primitive in a given classe, using annotations. <br/>
     * @param primName name of primitive to look for
     *                 - should not be <tt>null</tt>
     * @param cls class to search for primitive implementation
     *            - should not be <tt>null</tt>
     * @return a method implementing the primitive
     * @throws CadmiumException if the classes is not a valid
     *                              primitive provider
     * @throws CadmiumException if implementation is not found
     */
    private static Method lookupPrimitiveByAnnot(final String primName,
                                                 final Class cls)
        throws CadmiumException {
        assert primName != null : "null primName";
        assert cls != null : "null cls";
        for (Method m : cls.getDeclaredMethods()) {
            final String methName = m.getName();
            if (methName.equals(primName)
                && Modifier.isStatic(m.getModifiers())
                && (m.getAnnotation(Primitives.PRIMITIVE_ANNOT) != null)
                && m.getReturnType().equals(Value.class)
                && checkParameters(m.getParameterTypes())
                && checkExceptions(m.getExceptionTypes())) {
                return m;
            } // end if
        } // end for
        return null;
    } // end method 'lookupPrimitiveByAnnot(String, Class)'

    /**
     * Looks for a given primitive in a given classe, using fields. <br/>
     * @param primName name of primitive to look for
     *                 - should not be <tt>null</tt>
     * @param cls class to search for primitive implementation
     *            - should not be <tt>null</tt>
     * @return a method implementing the primitive
     * @throws CadmiumException if the classes is not a valid
     *                              primitive provider
     * @throws CadmiumException if implementation is not found
     */
    private static Method lookupPrimitiveByFields(final String primName,
                                                  final Class<?> cls)
        throws CadmiumException {
        assert primName != null : "null primName";
        assert cls != null : "null cls";
        final List<String> exportedPrimitives;
        final int[] exportedArities;
        try {
            final Field f = cls.getField(Primitives.EXPORTED_NAMES_FIELD);
            if (!Modifier.isStatic(f.getModifiers())
                || !f.getType().equals(String[].class)) {
                throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cls.getName()));
            } // end if
            exportedPrimitives = Arrays.asList((String[]) f.get(null));
            final Field f2 = cls.getField(Primitives.EXPORTED_ARITIES_FIELD);
            if (!Modifier.isStatic(f2.getModifiers())
                || !f2.getType().equals(int[].class)) {
                throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cls.getName()));
            } // end if
            exportedArities = (int[]) f2.get(null);
        } catch (final NoSuchFieldException nsfe) {
            throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cls.getName()));
        } catch (final IllegalAccessException iae) {
            throw new CadmiumException(String.format(Primitives.UNACCESSIBLE_EXPORT_FIELD, cls.getName()));
        } // end try/catch
        if (exportedPrimitives.size() != exportedArities.length) {
            throw new CadmiumException(String.format(Primitives.INCOHERENT_EXPORT_FIELDS, cls.getName()));
        } // end if
        final int idx = exportedPrimitives.indexOf(primName);
        if (idx >= 0) {
            try {
                final int arity = exportedArities[idx];
                if ((arity < 1) || (arity > 15)) {
                    throw new CadmiumException(String.format(Primitives.INVALID_ARITY_FOR_PRIM,
                                                             primName,
                                                             arity));
                } // end if
                final Method m = cls.getDeclaredMethod(primName,
                                                       makeSignature(arity));
                if (Modifier.isStatic(m.getModifiers())
                    && m.getReturnType().equals(Value.class)
                    && checkExceptions(m.getExceptionTypes())) {
                    return m;
                } else {
                    throw new CadmiumException(String.format(Primitives.NO_IMPLEMENTATION_OF_EXPORTED, cls.getName(), primName));
                } // end if/else
            } catch (final NoSuchMethodException nsme) {
                throw new CadmiumException(String.format(Primitives.NO_IMPLEMENTATION_OF_EXPORTED, cls.getName(), primName));
            } // end try/catch
        } else {
            return null;
        } // end if/else
    } // end method 'lookupPrimitiveByFields(String, Class)'

    /**
     * Loads all primitives from a given class.
     * @param prims where to put loaded primitives
     *               - should not be <tt>null</tt>
     * @param cl class to load primitives from - should not be <tt>null</tt>
     * @throws CadmiumException if the class is not a valid primitive
     *                              provider
     */
    static void loadAllPrimitives(final Map<String, Method> prims,
                                  final Class<?> cl)
        throws CadmiumException {
        assert prims != null : "null prims";
        assert cl != null : "null cl";
        if (cl.getAnnotation(Primitives.PRIMITIVE_PROVIDER_ANNOT) != null) {
            loadAllPrimitivesByAnnot(prims, cl);
        } else {
            loadAllPrimitivesByFields(prims, cl);
        } // end if/else
    } // end method 'loadAllPrimitives(Map<String, Method>, Class)'

    /**
     * Loads all primitives from a given class, using annotations.
     * @param prims where to put loaded primitives
     *               - should not be <tt>null</tt>
     * @param cl class to load primitives from - should not be <tt>null</tt>
     * @throws CadmiumException if the class is not a valid primitive
     *                              provider
     */
    private static void loadAllPrimitivesByAnnot(final Map<String, Method> prims,
                                                 final Class cl)
        throws CadmiumException {
        assert prims != null : "null prims";
        assert cl != null : "null cl";
        for (Method m : cl.getDeclaredMethods()) {
            final String primName = m.getName();
            if (Modifier.isStatic(m.getModifiers())
                && (m.getAnnotation(Primitives.PRIMITIVE_ANNOT) != null)
                && m.getReturnType().equals(Value.class)
                && checkParameters(m.getParameterTypes())
                && checkExceptions(m.getExceptionTypes())) {
                if (prims.put(primName, m) != null) {
                    throw new CadmiumException(String.format(Primitives.PRIM_DEF_TWICE, primName));
                } // end if
            } // end if
        } // end for
    } // end method 'loadAllPrimitivesByAnnot(Map<String, Method>, Class)'

    /**
     * Loads all primitives from a given class, using fields.
     * @param prims where to put loaded primitives
     *               - should not be <tt>null</tt>
     * @param cl class to load primitives from - should not be <tt>null</tt>
     * @throws CadmiumException if the class is not a valid primitive
     *                              provider
     */
    private static void loadAllPrimitivesByFields(final Map<String, Method> prims,
                                                  final Class cl)
        throws CadmiumException {
        assert prims != null : "null prims";
        assert cl != null : "null cl";
        final List<String> exportedPrimitives;
        final int[] exportedArities;
        final List<String> found = new LinkedList<String>();
        try {
            final Field f = cl.getField(Primitives.EXPORTED_NAMES_FIELD);
            if (!Modifier.isStatic(f.getModifiers())
                || !f.getType().equals(String[].class)) {
                throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cl.getName()));
            } // end if
            exportedPrimitives = // needed to get a mutable list (removeAll)
                new ArrayList<String>(Arrays.asList((String[]) f.get(null)));
            final Field f2 = cl.getField(Primitives.EXPORTED_ARITIES_FIELD);
            if (!Modifier.isStatic(f2.getModifiers())
                || !f2.getType().equals(int[].class)) {
                throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cl.getName()));
            } // end if
            exportedArities = (int[]) f2.get(null);
        } catch (final NoSuchFieldException nsfe) {
            throw new CadmiumException(String.format(Primitives.INVALID_PROVIDER, cl.getName()));
        } catch (final IllegalAccessException iae) {
            throw new CadmiumException(String.format(Primitives.UNACCESSIBLE_EXPORT_FIELD, cl.getName()));
        } // end try/catch
        if (exportedPrimitives.size() != exportedArities.length) {
            throw new CadmiumException(String.format(Primitives.INCOHERENT_EXPORT_FIELDS, cl.getName()));
        } // end if
        for (Method m : cl.getDeclaredMethods()) {
            final String primName = m.getName();
            final int idx = exportedPrimitives.indexOf(primName);
            final Class[] params = m.getParameterTypes();
            final int paramsLen = params.length;
            if ((idx >= 0)
                && Modifier.isStatic(m.getModifiers())
                && m.getReturnType().equals(Value.class)
                && checkParameters(m.getParameterTypes())
                && (m.getParameterTypes().length == exportedArities[idx] + 1)
                && checkExceptions(m.getExceptionTypes())) {
                found.add(primName);
                if (prims.put(primName, m) != null) {
                    throw new CadmiumException(String.format(Primitives.PRIM_DEF_TWICE, primName));
                } // end if
            } // end if
        } // end for
        if (found.size() < exportedPrimitives.size()) {
            exportedPrimitives.removeAll(found);
            throw new CadmiumException(String.format(Primitives.NO_IMPLEMENTATION_OF_EXPORTED, cl.getName(), exportedPrimitives.get(0)));
        } // end if
    } // end method 'loadAllPrimitivesByFields(Map<String, Method>, Class)'

    /**
     * Constructs an array of classes reprensenting the signature of
     * a primitive.
     * @param arity primitive arity - should be > 0
     * @return an array of length <tt>arity + 1</tt>
     *         (first element representing <tt>CodeRunner</tt> class, and
     *         other elements representing <tt>Value</tt> class)
     */
    private static Class[] makeSignature(final int arity) {
        assert arity > 0 : "arity should be > 0";
        final int nbParams = arity + 1;
        final Class[] primParams = new Class[nbParams];
        primParams[0] = CodeRunner.class;
        for (int i = 1; i < nbParams; i++) {
            primParams[i] = Value.class;
        } // end for
        return primParams;
    } // end method 'makeSignature(int)'

    /**
     * Checks that an array of types is a valid primitive signatures.
     * @param types type parameters to check
     * @return <tt>true</tt> if the array contains at least two elements the
     *         first one being <tt>CodeRunner</tt> and the other ones
     *         being <tt>Value</tt>,
     *         <tt>false</tt> otherwise
     */
    private static boolean checkParameters(final Class[] types) {
        assert types != null : "null types";
        final int len = types.length;
        if ((len >= 2) && (len <= 16)) {
            int i = 1;
            while ((i < len) && (Value.class.equals(types[i]))) {
                i++;
            } // end while
            return (i == len) && CodeRunner.class.equals(types[0]);
        } else {
            return false;
        } // end if/else
    } // end method 'checkParameters(Class[])'

    /**
     * Checks that an array of classes contains at most
     * {@link fr.x9c.cadmium.kernel.Fail.Exception} and
     * {@link fr.x9c.cadmium.kernel.Fatal.Exception}.
     * @param exceptions array to check - should not be <tt>null</tt>
     * @return <tt>true</tt> if <tt>exceptions</tt> contains
     * {@link fr.x9c.cadmium.kernel.Fail.Exception} and/or
     * {@link fr.x9c.cadmium.kernel.Fatal.Exception} or none of them,
     *        and no other class, <tt>false</tt> otherwise
     */
    private static boolean checkExceptions(final Class[] exceptions) {
        assert exceptions != null : "null exceptions";
        final int len = exceptions.length;
        for (int i = 0; i < len; i++) {
            final Class exn = exceptions[i];
            if (!exn.equals(Fail.Exception.class)
                && !exn.equals(Fatal.Exception.class)
                && !exn.equals(FalseExit.class)) {
                return false;
            } // end if
        } // end for
        return true;
    } // method 'checkExceptions(Class[])'

} // end class 'Primitives'
